Repository navigation
This change versions the Lens API by serving every public route under /v1 while keeping the existing unversioned paths as deprecated, behavior-identical aliases. Versioning is handled centrally with a Fastify rewriteUrl hook instead of mounting routes twice, so both forms resolve to the same handler and cannot drift. - #427
Conversation
|
@DevScoopee Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
… /v1 while keeping the existing unversioned paths as deprecated, behavior-identical aliases. Versioning is handled centrally with a Fastify rewriteUrl hook instead of mounting routes twice, so both forms resolve to the same handler and cannot drift. The routes previously had no version segment, which made the in-flight response-shape changes impossible to ship without breaking existing callers. To fix this, the /v1 segment is stripped before routing so /v1/price/... and /price/... hit one identical route definition, and requests using the deprecated alias get Deprecation: true and a Link: </v1/...>; rel="successor-version" header pointing at the versioned path. I added path helpers to src/openapi/coverage.ts, wired the rewrite and warning hook in src/app.ts, moved all public paths in openapi.yaml under /v1 (regenerating openapi.json), and extended tests/openapi.test.ts to assert that every documented versioned path maps to a registered alias, that the alias always exists, and that both forms answer identically. I also wrote the warning contract and retirement window in docs/api-versioning.md, updated the README endpoint table, and added a minor changeset. No callers break because the unversioned paths still answer identically Closes Divinity-Labs#264
tsc --noEmit clean vitest 91 files, 1052 passing (up from 86 / 962 on main) #251 read and scale token decimals from the chain #307 standardise empty-result vs not-found status codes #318 validate the live service against the published OpenAPI #324 content-derived strong ETag and honour conditional requests #326 cover vwap and the aggregator #388 persist permanent webhook delivery failures BREAKING, and deliberate: #307 changes routes that returned 404 for "no data" to 200 with an explicit reason. The rule is now 404 = the resource does not exist (pair not watched, key not found), 200 + empty = the query matched nothing. Our dropped-test guard surfaced it as - src/__tests__/basket.test.ts :: returns 404 when an asset has no price data which turned out to be renamed rather than deleted, to "returns 200 with error and reason when an asset has no price data". That is the guard working as intended: it made a behaviour change a decision instead of an accident. Any client branching on 404 for empty results needs updating; #427 (API versioning) is open and is where this belongs long-term. Three fixes applied on merge: - #388 and #318 each added a console.* call to non-test source, which the repo's own logging-hygiene test forbids. Both now use createLogger, which is what delivery.ts was already doing two lines above the offending call. - #326's test asserted CONFIDENCE_HIGH/MEDIUM_THRESHOLD_SECONDS, which do not exist. The constants are real and the values match, but the shipped names are HIGH_/MEDIUM_CONFIDENCE_MAX_AGE_SECONDS, from #545. Updated the test to the shipped names rather than renaming source to fit a stale test. - npx prisma generate, for #388's WebhookDeliveryFailure model. NOT merged: #384 (periodic WebSocket ping) does not compile -- it reads config.ws and never adds it to src/config.ts, and it drops a fanout test. #254 conflicts. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Miracle656
left a comment
There was a problem hiding this comment.
The branch does not build. src/app.ts calls six functions it no longer imports, so the app cannot boot and the test suite that would have caught the rest never runs.
src/app.ts(145,7): error TS2304: Cannot find name 'rate_limit_rejections_total'.
src/app.ts(174,3): error TS2304: Cannot find name 'registerGlobalRateLimit'.
src/app.ts(174,32): error TS2304: Cannot find name 'parseGlobalRateLimit'.
src/app.ts(204,22): error TS2304: Cannot find name 'registerCachePolicy'.
src/app.ts(224,9): error TS2552: Cannot find name 'registerFiatRoutes'.
src/app.ts(226,9): error TS2552: Cannot find name 'registerHealthRoutes'.
This came from the Merge branch 'main' into There-is-no-version-in-the-URL commit (9beacda), which kept each call and dropped the matching import. main has 42 imports in that file; this branch has 38. For example main carries
import { registerGlobalRateLimit, parseGlobalRateLimit } from './middleware/globalRateLimit'at src/app.ts:40, and the call survives here at line 174 with nothing importing it.
What that hides
tests/openapi.test.ts boots the app in beforeAll, so it throws ReferenceError: registerGlobalRateLimit is not defined and all 11 tests report as skipped rather than failed — including the new coverage guard this PR adds. The suite that exists to prove the spec matches the routes is the one that cannot run.
Across the whole suite: 7 files failing, 10 tests failing, 56 skipped.
What it would have caught
openapi.yaml goes from 35 documented paths to 25, and ten endpoints disappear from the spec entirely — not re-added under /v1:
book/{assetA}/{assetB} correlation impact/{assetA}/{assetB}
live movers price/{asset}/fiat
ready snapshot usage
venues/registry
Some of those are a direct consequence of the same botched merge — registerHealthRoutes and registerFiatRoutes are two of the six dropped imports, so /live, /ready and /price/{asset}/fiat are not merely undocumented, their registration is gone. /live and /ready are the health probes.
The others (/movers, /correlation, /book, /impact, /snapshot, /usage, /venues/registry) need checking individually: either they want /v1 entries in the spec, or they belong in INTERNAL_ROUTES with a reason.
The design is right
I want to be clear that the approach is good and worth finishing. A central rewriteUrl hook instead of mounting every route twice is the correct call — two registrations are two things that can drift. src/openapi/coverage.ts is better than what it replaces: every exclusion carries a reason, prefixes are kept separate from exact matches so a broad prefix cannot quietly excuse a public route, and UNDOCUMENTABLE_OPERATIONS distinguishes "this path is private" from "this one method cannot be expressed in OpenAPI 3.0". The note about QUERY /graphql needing 3.2 is exactly the kind of thing that should be written down.
What to do
Rebase on current main rather than merging it again — the branch is 29 commits behind now, and a rebase makes a dropped import show up as a conflict instead of a silent deletion. Then npx tsc --noEmit and npx vitest run both clean locally before re-requesting: tsc catches all six of these in one command.
One thing to decide while you are there: the spec documents only /v1/... paths, so the unversioned aliases this PR deliberately keeps are invisible to anyone reading the published document. If they are supported, they should probably appear as deprecated entries; if the spec is meant to describe only the versioned surface, say so in docs/api-versioning.md so the omission reads as a decision.
Miracle656
left a comment
There was a problem hiding this comment.
The versioning design is right and I want it. It is blocked on one thing: the spec in this branch is a regeneration of an older main, so merging it would undocument thirteen live routes.
The approach is the correct one
// Rather than registering each route twice (which would double the surface and,
// more importantly, let the two copies drift in middleware behaviour — x402
// gating, auth and rate limits all key off the *routed* path), the version
// segment is stripped before Fastify routes the request.That is exactly the reasoning I would want. Mounting every route twice is the obvious implementation and the wrong one: /v1/admin/keys and /admin/keys would be two route objects, and the moment one gains a config the other does not, they answer differently — which on the admin and x402 paths means an auth or payment gate that applies to one spelling and not the other. A rewriteUrl that collapses both onto a single registration cannot drift, because there is only one thing to drift.
I checked the two guards that landed today against it, and both hold: rewriteUrl runs before routing, so the CORS classifier (#316) and the admin auth hook (#574) both see the bare path — /v1/admin/keys is classified admin and goes through registerAdminAuth exactly as /admin/keys does. No bypass.
Keeping the unversioned form as a deprecated alias with warning headers, and identifying the deprecated caller from req.raw.originalUrl after the rewrite, is the right way round too.
Blocking: the spec would lose thirteen routes
openapi.yaml in this branch has 25 paths. main has 38. These thirteen exist on main and appear in this branch under no form, versioned or bare:
/book/{assetA}/{assetB} /live /rates/{asset}
/correlation /movers /ready
/discovery/search /price/{asset}/fiat /snapshot
/impact/{assetA}/{assetB} /tvl /usage
/venues/registry
I merged main into the branch locally to check whether git would restore them. It does not — only README.md and src/app.ts conflict, and openapi.yaml merges cleanly to 25 paths, because relative to the merge base the branch genuinely deletes them. Git has no way to tell "regenerated from an older spec" from "deliberately removed".
The blast radius is wider than the spec file, because three other things are generated from it:
openapi.json(CI byte-compares it),clients/typescript/src/schema.d.ts— the published client would lose those thirteen endpoints,- the README endpoint table, which
tests/readme.test.tsderives fromopenapi.yaml.
So a merge would quietly remove /tvl, /movers, /correlation and the rest from the documentation, the typed client and the README at once, while the routes keep serving traffic.
What to do
Rebase onto main rather than merging it. A rebase replays your versioning change on top of the current spec, so the thirteen routes are simply present and get /v1 forms like everything else. Merging asks git to reconcile two whole-file rewrites, which is how the deletions survive.
After the rebase:
- Re-apply the
/v1prefix across all 38 paths, not the 25 that existed when you branched. npm run openapi:gen— do not hand-editopenapi.json; it is byte-compared in CI.npx --yes openapi-typescript@7.13.0 openapi.yaml -o clients/typescript/src/schema.d.ts(npm run client:genneeds the binary on PATH).npm run readme:gen— the README endpoint table comes from the spec now, and its test will fail otherwise.npx tsc --noEmit, thennpm test.
Your addition to src/openapi/coverage.ts should make the first point self-checking — if it compares registered routes against documented paths, a missing /tvl ought to fail the suite rather than wait for a reviewer to diff two files. Worth confirming it fires on this exact case, because that guard is the thing that stops this recurring.
One question on the deprecation contract while you are in there: docs/api-versioning.md describes the warning headers, but the window itself matters more. Installed Veil APKs call these routes with no /v1, and they cannot be rebuilt on demand — so the unversioned alias has to outlive the install base, not a calendar date. Worth saying that explicitly in the doc rather than naming a removal date someone might act on.
Ping me when it is rebased. The hard part is already done and correct; this is the spec catching up.
|
Merged into 1. The spec dropped 17 routes.
They would have disappeared from the published spec and from the generated TypeScript client. 2.
3. README lost its 4. Three test files assumed bare paths. I also restored On the "No test case silently removed" check — that was a false alarm. You renamed
One general note for next time: |
The routes previously had no version segment, which made the in-flight response-shape changes impossible to ship without breaking existing callers. To fix this, the /v1 segment is stripped before routing so /v1/price/... and /price/... hit one identical route definition, and requests using the deprecated alias get Deprecation: true and a Link: </v1/...>; rel="successor-version" header pointing at the versioned path. I added path helpers to src/openapi/coverage.ts, wired the rewrite and warning hook in src/app.ts, moved all public paths in openapi.yaml under /v1 (regenerating openapi.json), and extended tests/openapi.test.ts to assert that every documented versioned path maps to a registered alias, that the alias always exists, and that both forms answer identically. I also wrote the warning contract and retirement window in docs/api-versioning.md, updated the README endpoint table, and added a minor changeset. No callers break because the unversioned paths still answer identically
Closes #264